package com.badlogic.gdx.graphics.g3d.particles.batches;

import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.attributes.BlendingAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.DepthTestAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.particles.ParallelArray.FloatChannel;
import com.badlogic.gdx.graphics.g3d.particles.ParticleChannels;
import com.badlogic.gdx.graphics.g3d.particles.ParticleShader;
import com.badlogic.gdx.graphics.g3d.particles.ParticleShader.AlignMode;
import com.badlogic.gdx.graphics.g3d.particles.ResourceData;
import com.badlogic.gdx.graphics.g3d.particles.ResourceData.SaveData;
import com.badlogic.gdx.graphics.g3d.particles.renderers.BillboardControllerRenderData;
import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Pool;


/** This class is used to render billboard particles.
 * @author Inferno */
public class BillboardParticleBatch extends BufferedParticleBatch<BillboardControllerRenderData> {
	protected static final Vector3 TMP_V1 = new Vector3(),
		TMP_V2 = new Vector3(),
		TMP_V3 = new Vector3(),
		TMP_V4 = new Vector3(),
		TMP_V5 = new Vector3(),
		TMP_V6 = new Vector3();
	protected static final Matrix3 TMP_M3 = new Matrix3();
	//Attributes
	protected static final int sizeAndRotationUsage = 1 << 9, directionUsage = 1 << 10;
	private static final VertexAttributes
	GPU_ATTRIBUTES = new VertexAttributes(new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE),
		new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE+"0"),
		new VertexAttribute(Usage.ColorUnpacked, 4, ShaderProgram.COLOR_ATTRIBUTE),
		new VertexAttribute(sizeAndRotationUsage, 4, "a_sizeAndRotation")),
		/*
		GPU_EXT_ATTRIBUTES = new VertexAttributes(new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE),
									new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE+"0"),
									new VertexAttribute(Usage.Color, 4, ShaderProgram.COLOR_ATTRIBUTE),
									new VertexAttribute(sizeAndRotationUsage, 4, "a_sizeAndRotation"),
									new VertexAttribute(directionUsage, 3, "a_direction")),
		 */
		CPU_ATTRIBUTES = new VertexAttributes(	new VertexAttribute(Usage.Position, 3, ShaderProgram.POSITION_ATTRIBUTE),
			new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE+"0"),
			new VertexAttribute(Usage.ColorUnpacked, 4, ShaderProgram.COLOR_ATTRIBUTE) );

	//Offsets
	private static final int 	GPU_POSITION_OFFSET = (short)(GPU_ATTRIBUTES.findByUsage(Usage.Position).offset/4),
		GPU_UV_OFFSET = (short)(GPU_ATTRIBUTES.findByUsage(Usage.TextureCoordinates).offset/4),
		GPU_SIZE_ROTATION_OFFSET = (short)(GPU_ATTRIBUTES.findByUsage(sizeAndRotationUsage).offset/4),
		GPU_COLOR_OFFSET = (short)(GPU_ATTRIBUTES.findByUsage(Usage.ColorUnpacked).offset/4),
		GPU_VERTEX_SIZE = GPU_ATTRIBUTES.vertexSize/4,

		//Ext
		/*
										GPU_EXT_POSITION_OFFSET = (short)(GPU_EXT_ATTRIBUTES.findByUsage(Usage.Position).offset/4),
										GPU_EXT_UV_OFFSET = (short)(GPU_EXT_ATTRIBUTES.findByUsage(Usage.TextureCoordinates).offset/4),
										GPU_EXT_SIZE_ROTATION_OFFSET = (short)(GPU_EXT_ATTRIBUTES.findByUsage(sizeAndRotationUsage).offset/4),
										GPU_EXT_COLOR_OFFSET = (short)(GPU_EXT_ATTRIBUTES.findByUsage(Usage.Color).offset/4),
										GPU_EXT_DIRECTION_OFFSET = (short)(GPU_EXT_ATTRIBUTES.findByUsage(directionUsage).offset/4),
										GPU_EXT_VERTEX_SIZE = GPU_EXT_ATTRIBUTES.vertexSize/4,
		 */

		//Cpu
		CPU_POSITION_OFFSET = (short)(CPU_ATTRIBUTES.findByUsage(Usage.Position).offset/4),
		CPU_UV_OFFSET = (short)(CPU_ATTRIBUTES.findByUsage(Usage.TextureCoordinates).offset/4),
		CPU_COLOR_OFFSET = (short)(CPU_ATTRIBUTES.findByUsage(Usage.ColorUnpacked).offset/4),
		CPU_VERTEX_SIZE= CPU_ATTRIBUTES.vertexSize/4;
	private final static int 	MAX_PARTICLES_PER_MESH = Short.MAX_VALUE/4,
		MAX_VERTICES_PER_MESH = MAX_PARTICLES_PER_MESH*4;

	private class RenderablePool extends Pool<Renderable>{
		public RenderablePool () {}

		@Override
		public Renderable newObject () {
			return allocRenderable();
		}
	}

	public static class Config{
		public Config(){}
		public Config (boolean useGPU, AlignMode mode) {
			this.useGPU = useGPU;
			this.mode = mode;
		}
		boolean useGPU;
		AlignMode mode;
	}

	private RenderablePool renderablePool;
	private Array<Renderable> renderables;
	private float[] vertices;
	private short[] indices;
	private int currentVertexSize = 0;
	private VertexAttributes currentAttributes;
	protected boolean useGPU = false;
	protected AlignMode mode = AlignMode.Screen;
	protected Texture texture;
	protected BlendingAttribute blendingAttribute;
	protected DepthTestAttribute depthTestAttribute;
	Shader shader;

	/**
	 * Create a new BillboardParticleBatch
	 * @param mode
	 * @param useGPU Allow to use GPU instead of CPU
	 * @param capacity Max particle displayed
	 * @param blendingAttribute Blending attribute used by the batch
	 * @param depthTestAttribute DepthTest attribute used by the batch
	 */
	public BillboardParticleBatch(AlignMode mode, boolean useGPU, int capacity,
		BlendingAttribute blendingAttribute,
		DepthTestAttribute depthTestAttribute) {
		super(BillboardControllerRenderData.class);
		renderables = new Array<Renderable>();
		renderablePool = new RenderablePool();
		this.blendingAttribute = blendingAttribute;
		this.depthTestAttribute = depthTestAttribute;

		if( this.blendingAttribute == null )
			this.blendingAttribute = new BlendingAttribute(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA, 1f);
		if( this.depthTestAttribute == null )
			this.depthTestAttribute = new DepthTestAttribute(GL20.GL_LEQUAL, false);

		allocIndices();
		initRenderData();
		ensureCapacity(capacity);
		setUseGpu(useGPU);
		setAlignMode(mode);
	}

	public BillboardParticleBatch(AlignMode mode, boolean useGPU, int capacity){
		this(mode, useGPU, capacity, null, null);
	}

	public BillboardParticleBatch () {
		this(AlignMode.Screen, false, 100);
	}

	public BillboardParticleBatch (int capacity) {
		this(AlignMode.Screen, false, capacity);
	}

	@Override
	public void allocParticlesData(int capacity){
		vertices = new float[currentVertexSize*4*capacity];
		allocRenderables(capacity);
	}

	protected Renderable allocRenderable(){
		Renderable renderable = new Renderable();
		renderable.primitiveType = GL20.GL_TRIANGLES;
		renderable.meshPartOffset = 0;
		renderable.material = new Material(this.blendingAttribute, this.depthTestAttribute,
			TextureAttribute.createDiffuse(texture));
		renderable.mesh = new Mesh(false, MAX_VERTICES_PER_MESH, MAX_PARTICLES_PER_MESH*6, currentAttributes);
		renderable.mesh.setIndices(indices);
		renderable.shader = shader;
		return renderable;
	}

	private void allocIndices(){
		int indicesCount = MAX_PARTICLES_PER_MESH * 6;
		indices = new short[indicesCount];
		for(int i=0, vertex = 0; i < indicesCount; i+=6, vertex+=4){
			indices[i] = (short)vertex;
			indices[i+1] = (short)(vertex+1);
			indices[i+2] = (short)(vertex+2);
			indices[i+3] = (short)(vertex+2);
			indices[i+4] = (short)(vertex+3);
			indices[i+5] = (short)vertex;
		}
	}

	private void allocRenderables(int capacity){
		//Free old meshes
		int 	meshCount = MathUtils.ceil( capacity/MAX_PARTICLES_PER_MESH),
			free = renderablePool.getFree();
		if(free < meshCount){
			for(int i=0, left = meshCount - free; i < left;++i)
				renderablePool.free(renderablePool.newObject());
		}
	}

	private Shader getShader (Renderable renderable) {
		Shader shader = useGPU 	? 	new ParticleShader(renderable, new ParticleShader.Config(mode)) :
			new DefaultShader(renderable);
		shader.init();
		return shader;
	}

	private void allocShader () {
		Renderable newRenderable = allocRenderable();
		shader = newRenderable.shader = getShader(newRenderable);
		renderablePool.free(newRenderable);
	}

	private void clearRenderablesPool(){
		renderablePool.freeAll(renderables);
		for(int i=0, free = renderablePool.getFree(); i < free; ++i){
			Renderable renderable = renderablePool.obtain();
			renderable.mesh.dispose();
		}
		renderables.clear();
	}

	/** Sets vertex attributes and size */
	public void setVertexData(){
		if(useGPU){
			currentAttributes = GPU_ATTRIBUTES;
			currentVertexSize = GPU_VERTEX_SIZE;
			/*
			if(mode == AlignMode.ParticleDirection){
				currentAttributes = GPU_EXT_ATTRIBUTES;
				currentVertexSize = GPU_EXT_VERTEX_SIZE;
			}
			else{
				currentAttributes = GPU_ATTRIBUTES;
				currentVertexSize = GPU_VERTEX_SIZE;
			}
			 */
		}
		else {
			currentAttributes = CPU_ATTRIBUTES;
			currentVertexSize = CPU_VERTEX_SIZE;
		}
	}

	/** Allocates all the require rendering resources like Renderables,Shaders,Meshes
	 *  according to the current batch configuration.*/
	private void initRenderData () {
		setVertexData();
		clearRenderablesPool();
		allocShader();
		resetCapacity();
	}

	/** Sets the current align mode.
	 *  It will reallocate internal data, use only when necessary. */
	public void setAlignMode(AlignMode mode){
		if(mode != this.mode){
			this.mode = mode;
			if(useGPU){
				initRenderData();
				allocRenderables(bufferedParticlesCount);
			}
		}
	}

	public AlignMode getAlignMode(){
		return mode;
	}

	/** Sets the current align mode.
	 *  It will reallocate internal data, use only when necessary. */
	public void setUseGpu(boolean useGPU){
		if(this.useGPU != useGPU){
			this.useGPU = useGPU;
			initRenderData();
			allocRenderables(bufferedParticlesCount);
		}
	}

	public boolean isUseGPU(){
		return useGPU;
	}

	public void setTexture(Texture texture){
		renderablePool.freeAll(renderables);
		renderables.clear();
		for(int i=0, free = renderablePool.getFree(); i < free; ++i){
			Renderable renderable = renderablePool.obtain();
			TextureAttribute attribute = (TextureAttribute) renderable.material.get(TextureAttribute.Diffuse);
			attribute.textureDescription.texture = texture;
		}
		this.texture = texture;
	}

	public Texture getTexture () {
		return texture;
	}

	@Override
	public void begin () {
		super.begin();
		renderablePool.freeAll(renderables);
		renderables.clear();
	}

	//GPU
	//Required + Color + Rotation
	private static void putVertex( float[] vertices,  int offset, float x, float y, float z, float u, float v, float scaleX, float scaleY,  float cosRotation, float sinRotation, float r, float g, float b, float a) {
		//Position
		vertices[offset + GPU_POSITION_OFFSET] = x;
		vertices[offset + GPU_POSITION_OFFSET+1] = y;
		vertices[offset + GPU_POSITION_OFFSET+2] = z;
		//UV
		vertices[offset + GPU_UV_OFFSET] = u;
		vertices[offset + GPU_UV_OFFSET+1] = v;
		//Scale
		vertices[offset + GPU_SIZE_ROTATION_OFFSET] = scaleX;
		vertices[offset + GPU_SIZE_ROTATION_OFFSET+1] = scaleY;
		vertices[offset + GPU_SIZE_ROTATION_OFFSET+2] = cosRotation;
		vertices[offset + GPU_SIZE_ROTATION_OFFSET+3] = sinRotation;
		//Color
		vertices[offset + GPU_COLOR_OFFSET] = r;
		vertices[offset + GPU_COLOR_OFFSET+1] = g;
		vertices[offset + GPU_COLOR_OFFSET+2] = b;
		vertices[offset + GPU_COLOR_OFFSET+3] = a;
	}

	/*
	//Required + Color + Rotation + Direction
	private static void putVertex( float[] vertices,  int offset, float x, float y, float z, float u, float v, float scaleX, float scaleY,  float cosRotation, float sinRotation, float r, float g, float b, float a, Vector3 direction) {
		//Position
		vertices[offset + GPU_EXT_POSITION_OFFSET] = x;
		vertices[offset + GPU_EXT_POSITION_OFFSET+1] = y;
		vertices[offset + GPU_EXT_POSITION_OFFSET+2] = z;
		//UV
		vertices[offset + GPU_EXT_UV_OFFSET] = u;
		vertices[offset + GPU_EXT_UV_OFFSET+1] = v;
		//Scale
		vertices[offset + GPU_EXT_SIZE_ROTATION_OFFSET] = scaleX;
		vertices[offset + GPU_EXT_SIZE_ROTATION_OFFSET+1] = scaleY;
		vertices[offset + GPU_EXT_SIZE_ROTATION_OFFSET+2] = cosRotation;
		vertices[offset + GPU_EXT_SIZE_ROTATION_OFFSET+3] = sinRotation;
		//Color
		vertices[offset + GPU_EXT_COLOR_OFFSET] = r;
		vertices[offset + GPU_EXT_COLOR_OFFSET+1] = g;
		vertices[offset + GPU_EXT_COLOR_OFFSET+2] = b;
		vertices[offset + GPU_EXT_COLOR_OFFSET+3] = a;
		//Direction
		vertices[offset + GPU_EXT_DIRECTION_OFFSET] = direction.x;
		vertices[offset + GPU_EXT_DIRECTION_OFFSET +1] = direction.y;
		vertices[offset + GPU_EXT_DIRECTION_OFFSET +2] = direction.z;
	}
	 */


	//CPU
	//Required
	private static void putVertex(float[] vertices, int offset, Vector3 p, float u, float v, float r, float g, float b, float a) {
		//Position
		vertices[offset + CPU_POSITION_OFFSET] = p.x;
		vertices[offset + CPU_POSITION_OFFSET+1] = p.y;
		vertices[offset + CPU_POSITION_OFFSET+2] = p.z;
		//UV
		vertices[offset + CPU_UV_OFFSET] = u;
		vertices[offset + CPU_UV_OFFSET+1] = v;
		//Color
		vertices[offset + CPU_COLOR_OFFSET] = r;
		vertices[offset + CPU_COLOR_OFFSET+1] = g;
		vertices[offset + CPU_COLOR_OFFSET+2] = b;
		vertices[offset + CPU_COLOR_OFFSET+3] = a;
	}

	private void fillVerticesGPU (int[] particlesOffset) {
		int tp=0;
		for(BillboardControllerRenderData data : renderData){
			FloatChannel scaleChannel = data.scaleChannel;
			FloatChannel regionChannel = data.regionChannel;
			FloatChannel positionChannel = data.positionChannel;
			FloatChannel colorChannel = data.colorChannel;
			FloatChannel rotationChannel = data.rotationChannel;
			for(int p=0, c = data.controller.particles.size; p < c; ++p, ++tp){
				int baseOffset = particlesOffset[tp]*currentVertexSize*4;
				float scale = scaleChannel.data[p* scaleChannel.strideSize];
				int regionOffset = p*regionChannel.strideSize;
				int positionOffset = p*positionChannel.strideSize;
				int colorOffset = p*colorChannel.strideSize;
				int rotationOffset = p*rotationChannel.strideSize;
				float 	px = positionChannel.data[positionOffset + ParticleChannels.XOffset],
					py = positionChannel.data[positionOffset + ParticleChannels.YOffset],
					pz = positionChannel.data[positionOffset + ParticleChannels.ZOffset];
				float u = regionChannel.data[regionOffset +ParticleChannels.UOffset];
				float v = regionChannel.data[regionOffset +ParticleChannels.VOffset];
				float u2 = regionChannel.data[regionOffset +ParticleChannels.U2Offset];
				float v2 = regionChannel.data[regionOffset +ParticleChannels.V2Offset];
				float 	sx = regionChannel.data[regionOffset +ParticleChannels.HalfWidthOffset] * scale,
					sy = regionChannel.data[regionOffset+ParticleChannels.HalfHeightOffset]  * scale;
				float r = colorChannel.data[colorOffset +ParticleChannels.RedOffset];
				float g = colorChannel.data[colorOffset +ParticleChannels.GreenOffset];
				float b = colorChannel.data[colorOffset +ParticleChannels.BlueOffset];
				float a = colorChannel.data[colorOffset +ParticleChannels.AlphaOffset];
				float cosRotation = rotationChannel.data[rotationOffset +ParticleChannels.CosineOffset];
				float sinRotation = rotationChannel.data[rotationOffset +ParticleChannels.SineOffset];

				//bottom left, bottom right, top right, top left
				putVertex(vertices, baseOffset, px, py, pz, u, v2, -sx, -sy, cosRotation, sinRotation, r, g, b, a);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u2, v2, sx, -sy, cosRotation, sinRotation, r, g, b, a);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u2, v, sx, sy, cosRotation, sinRotation, r, g, b, a);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u, v, -sx, sy, cosRotation, sinRotation, r, g, b, a);
				baseOffset += currentVertexSize;
			}
		}
	}

	/*
	private void fillVerticesToParticleDirectionGPU (int[] particlesOffset) {
		int tp=0;
		for(BillboardControllerRenderData data : renderData){
			FloatChannel scaleChannel = data.scaleChannel;
			FloatChannel regionChannel = data.regionChannel;
			FloatChannel positionChannel = data.positionChannel;
			FloatChannel colorChannel = data.colorChannel;
			FloatChannel rotationChannel = data.rotationChannel;

			for(int p=0, c = data.controller.particles.size; p < c; ++p, ++tp){
				int baseOffset = particlesOffset[tp]*currentVertexSize*4;
				float scale = scaleChannel.data[p* scaleChannel.strideSize];
				int regionOffset = p*regionChannel.strideSize;
				int positionOffset = p*positionChannel.strideSize;
				int colorOffset = p*colorChannel.strideSize;
				int rotationOffset = p*rotationChannel.strideSize;
				int velocityOffset = p* velocityChannel.strideSize;
				float 	px = positionChannel.data[positionOffset + ParticleChannels.XOffset],
					py = positionChannel.data[positionOffset + ParticleChannels.YOffset],
					pz = positionChannel.data[positionOffset + ParticleChannels.ZOffset];
				float u = regionChannel.data[regionOffset +ParticleChannels.UOffset];
				float v = regionChannel.data[regionOffset +ParticleChannels.VOffset];
				float u2 = regionChannel.data[regionOffset +ParticleChannels.U2Offset];
				float v2 = regionChannel.data[regionOffset +ParticleChannels.V2Offset];
				float 	sx = regionChannel.data[regionOffset +ParticleChannels.HalfWidthOffset] * scale,
							sy = regionChannel.data[regionOffset+ParticleChannels.HalfHeightOffset]  * scale;
				float r = colorChannel.data[colorOffset +ParticleChannels.RedOffset];
				float g = colorChannel.data[colorOffset +ParticleChannels.GreenOffset];
				float b = colorChannel.data[colorOffset +ParticleChannels.BlueOffset];
				float a = colorChannel.data[colorOffset +ParticleChannels.AlphaOffset];
				float cosRotation = rotationChannel.data[rotationOffset +ParticleChannels.CosineOffset];
				float sinRotation = rotationChannel.data[rotationOffset +ParticleChannels.SineOffset];
				float 	vx = velocityChannel.data[velocityOffset + ParticleChannels.XOffset],
							vy = velocityChannel.data[velocityOffset + ParticleChannels.YOffset],
							vz = velocityChannel.data[velocityOffset + ParticleChannels.ZOffset];

				//bottom left, bottom right, top right, top left
				TMP_V1.set(vx, vy, vz).nor();
				putVertex(vertices, baseOffset, px, py, pz, u, v2, -sx, -sy, cosRotation, sinRotation, r, g, b, a, TMP_V1);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u2, v2, sx, -sy, cosRotation, sinRotation, r, g, b, a, TMP_V1);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u2, v, sx, sy, cosRotation, sinRotation, r, g, b, a, TMP_V1);
				baseOffset += currentVertexSize;
				putVertex(vertices, baseOffset, px, py, pz, u, v, -sx, sy, cosRotation, sinRotation, r, g, b, a, TMP_V1);
			}
		}
	}

	private void fillVerticesToParticleDirectionCPU (int[] particlesOffset) {
		int tp=0;
		for(ParticleController controller : renderData){
			FloatChannel scaleChannel = controller.particles.getChannel(ParticleChannels.Scale);
			FloatChannel regionChannel = controller.particles.getChannel(ParticleChannels.TextureRegion);
			FloatChannel positionChannel = controller.particles.getChannel(ParticleChannels.Position);
			FloatChannel colorChannel = controller.particles.getChannel(ParticleChannels.Color);
			FloatChannel rotationChannel = controller.particles.getChannel(ParticleChannels.Rotation2D);
			FloatChannel velocityChannel = controller.particles.getChannel(ParticleChannels.Accelleration);

			for(int p=0, c = controller.particles.size; p < c; ++p, ++tp){
				int baseOffset = particlesOffset[tp]*currentVertexSize*4;
				float scale = scaleChannel.data[p* scaleChannel.strideSize];
				int regionOffset = p*regionChannel.strideSize;
				int positionOffset = p*positionChannel.strideSize;
				int colorOffset = p*colorChannel.strideSize;
				int rotationOffset = p*rotationChannel.strideSize;
				int velocityOffset = p* velocityChannel.strideSize;
				float 	px = positionChannel.data[positionOffset + ParticleChannels.XOffset],
					py = positionChannel.data[positionOffset + ParticleChannels.YOffset],
					pz = positionChannel.data[positionOffset + ParticleChannels.ZOffset];
				float u = regionChannel.data[regionOffset +ParticleChannels.UOffset];
				float v = regionChannel.data[regionOffset +ParticleChannels.VOffset];
				float u2 = regionChannel.data[regionOffset +ParticleChannels.U2Offset];
				float v2 = regionChannel.data[regionOffset +ParticleChannels.V2Offset];
				float 	sx = regionChannel.data[regionOffset +ParticleChannels.HalfWidthOffset] * scale,
							sy = regionChannel.data[regionOffset+ParticleChannels.HalfHeightOffset]  * scale;
				float r = colorChannel.data[colorOffset +ParticleChannels.RedOffset];
				float g = colorChannel.data[colorOffset +ParticleChannels.GreenOffset];
				float b = colorChannel.data[colorOffset +ParticleChannels.BlueOffset];
				float a = colorChannel.data[colorOffset +ParticleChannels.AlphaOffset];
				float cosRotation = rotationChannel.data[rotationOffset +ParticleChannels.CosineOffset];
				float sinRotation = rotationChannel.data[rotationOffset +ParticleChannels.SineOffset];
				float 	vx = velocityChannel.data[velocityOffset + ParticleChannels.XOffset],
							vy = velocityChannel.data[velocityOffset + ParticleChannels.YOffset],
							vz = velocityChannel.data[velocityOffset + ParticleChannels.ZOffset];
				Vector3 up = TMP_V1.set(vx,vy,vz).nor(),
								look = TMP_V3.set(camera.position).sub(px,py,pz).nor(), //normal
								right = TMP_V2.set(up).crs(look).nor(); //tangent
				look.set(right).crs(up).nor();
				right.scl(sx);
				up.scl(sy);

				if(cosRotation != 1){
					TMP_M3.setToRotation(look, cosRotation, sinRotation);
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x-TMP_V2.x, -TMP_V1.y-TMP_V2.y, -TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset,TMP_V6.set(TMP_V1.x-TMP_V2.x, TMP_V1.y-TMP_V2.y, TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset,TMP_V6.set(TMP_V1.x+TMP_V2.x, TMP_V1.y+TMP_V2.y, TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x, -TMP_V1.y+TMP_V2.y, -TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v, r, g, b, a);
				}
				else {
					putVertex(vertices, baseOffset,TMP_V6.set(-TMP_V1.x-TMP_V2.x+px, -TMP_V1.y-TMP_V2.y+py, -TMP_V1.z-TMP_V2.z+pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset,TMP_V6.set(TMP_V1.x-TMP_V2.x+px, TMP_V1.y-TMP_V2.y+py, TMP_V1.z-TMP_V2.z+pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset,TMP_V6.set(TMP_V1.x+TMP_V2.x+px, TMP_V1.y+TMP_V2.y+py, TMP_V1.z+TMP_V2.z+pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x+px, -TMP_V1.y+TMP_V2.y+py, -TMP_V1.z+TMP_V2.z+pz), u, v, r, g, b, a);
				}
			}
		}
	}
	 */

	private void fillVerticesToViewPointCPU (int[] particlesOffset) {
		int tp=0;
		for(BillboardControllerRenderData data : renderData){
			FloatChannel scaleChannel = data.scaleChannel;
			FloatChannel regionChannel = data.regionChannel;
			FloatChannel positionChannel = data.positionChannel;
			FloatChannel colorChannel = data.colorChannel;
			FloatChannel rotationChannel = data.rotationChannel;

			for(int p=0, c = data.controller.particles.size; p < c; ++p, ++tp){
				int baseOffset = particlesOffset[tp]*currentVertexSize*4;
				float scale = scaleChannel.data[p* scaleChannel.strideSize];
				int regionOffset = p*regionChannel.strideSize;
				int positionOffset = p*positionChannel.strideSize;
				int colorOffset = p*colorChannel.strideSize;
				int rotationOffset = p*rotationChannel.strideSize;
				float 	px = positionChannel.data[positionOffset + ParticleChannels.XOffset],
					py = positionChannel.data[positionOffset + ParticleChannels.YOffset],
					pz = positionChannel.data[positionOffset + ParticleChannels.ZOffset];
				float u = regionChannel.data[regionOffset +ParticleChannels.UOffset];
				float v = regionChannel.data[regionOffset +ParticleChannels.VOffset];
				float u2 = regionChannel.data[regionOffset +ParticleChannels.U2Offset];
				float v2 = regionChannel.data[regionOffset +ParticleChannels.V2Offset];
				float 	sx = regionChannel.data[regionOffset +ParticleChannels.HalfWidthOffset] * scale,
					sy = regionChannel.data[regionOffset+ParticleChannels.HalfHeightOffset]  * scale;
				float r = colorChannel.data[colorOffset +ParticleChannels.RedOffset];
				float g = colorChannel.data[colorOffset +ParticleChannels.GreenOffset];
				float b = colorChannel.data[colorOffset +ParticleChannels.BlueOffset];
				float a = colorChannel.data[colorOffset +ParticleChannels.AlphaOffset];
				float cosRotation = rotationChannel.data[rotationOffset +ParticleChannels.CosineOffset];
				float sinRotation = rotationChannel.data[rotationOffset +ParticleChannels.SineOffset];
				Vector3 look = TMP_V3.set(camera.position).sub(px, py, pz).nor(), //normal
					right = TMP_V1.set(camera.up).crs(look).nor(), //tangent
					up = TMP_V2.set(look).crs(right);
				right.scl(sx);
				up.scl(sy);

				if(cosRotation != 1){
					TMP_M3.setToRotation(look, cosRotation, sinRotation);
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x-TMP_V2.x, -TMP_V1.y-TMP_V2.y, -TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x-TMP_V2.x, TMP_V1.y-TMP_V2.y, TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x+TMP_V2.x, TMP_V1.y+TMP_V2.y, TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x, -TMP_V1.y+TMP_V2.y, -TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v, r, g, b, a);
				}
				else {
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x-TMP_V2.x+px, -TMP_V1.y-TMP_V2.y+py, -TMP_V1.z-TMP_V2.z+pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x-TMP_V2.x+px, TMP_V1.y-TMP_V2.y+py, TMP_V1.z-TMP_V2.z+pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x+TMP_V2.x+px, TMP_V1.y+TMP_V2.y+py, TMP_V1.z+TMP_V2.z+pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x+px, -TMP_V1.y+TMP_V2.y+py, -TMP_V1.z+TMP_V2.z+pz), u, v, r, g, b, a);
				}
			}
		}
	}

	private void fillVerticesToScreenCPU (int[] particlesOffset) {
		Vector3 look = TMP_V3.set(camera.direction).scl(-1),  //normal
			right = TMP_V4.set(camera.up).crs(look).nor(), //tangent
			up = camera.up;

		int tp=0;
		for(BillboardControllerRenderData data : renderData){
			FloatChannel scaleChannel = data.scaleChannel;
			FloatChannel regionChannel = data.regionChannel;
			FloatChannel positionChannel = data.positionChannel;
			FloatChannel colorChannel = data.colorChannel;
			FloatChannel rotationChannel = data.rotationChannel;

			for(int p=0, c = data.controller.particles.size; p < c; ++p, ++tp){
				int baseOffset = particlesOffset[tp]*currentVertexSize*4;
				float scale = scaleChannel.data[p* scaleChannel.strideSize];
				int regionOffset = p*regionChannel.strideSize;
				int positionOffset = p*positionChannel.strideSize;
				int colorOffset = p*colorChannel.strideSize;
				int rotationOffset = p*rotationChannel.strideSize;
				float 	px = positionChannel.data[positionOffset + ParticleChannels.XOffset],
					py = positionChannel.data[positionOffset + ParticleChannels.YOffset],
					pz = positionChannel.data[positionOffset + ParticleChannels.ZOffset];
				float u = regionChannel.data[regionOffset +ParticleChannels.UOffset];
				float v = regionChannel.data[regionOffset +ParticleChannels.VOffset];
				float u2 = regionChannel.data[regionOffset +ParticleChannels.U2Offset];
				float v2 = regionChannel.data[regionOffset +ParticleChannels.V2Offset];
				float 	sx = regionChannel.data[regionOffset +ParticleChannels.HalfWidthOffset] * scale,
					sy = regionChannel.data[regionOffset+ParticleChannels.HalfHeightOffset]  * scale;
				float r = colorChannel.data[colorOffset +ParticleChannels.RedOffset];
				float g = colorChannel.data[colorOffset +ParticleChannels.GreenOffset];
				float b = colorChannel.data[colorOffset +ParticleChannels.BlueOffset];
				float a = colorChannel.data[colorOffset +ParticleChannels.AlphaOffset];
				float cosRotation = rotationChannel.data[rotationOffset +ParticleChannels.CosineOffset];
				float sinRotation = rotationChannel.data[rotationOffset +ParticleChannels.SineOffset];
				TMP_V1.set(right).scl(sx);
				TMP_V2.set(up).scl(sy);

				if(cosRotation != 1){
					TMP_M3.setToRotation(look, cosRotation, sinRotation);
					putVertex( vertices, baseOffset, TMP_V6.set(-TMP_V1.x-TMP_V2.x, -TMP_V1.y-TMP_V2.y, -TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x-TMP_V2.x, TMP_V1.y-TMP_V2.y, TMP_V1.z-TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x+TMP_V2.x, TMP_V1.y+TMP_V2.y, TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x, -TMP_V1.y+TMP_V2.y, -TMP_V1.z+TMP_V2.z).mul(TMP_M3).add(px, py, pz), u, v, r, g, b, a);
				}
				else {
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x-TMP_V2.x+px, -TMP_V1.y-TMP_V2.y+py, -TMP_V1.z-TMP_V2.z+pz), u, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x-TMP_V2.x+px, TMP_V1.y-TMP_V2.y+py, TMP_V1.z-TMP_V2.z+pz), u2, v2, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(TMP_V1.x+TMP_V2.x+px, TMP_V1.y+TMP_V2.y+py, TMP_V1.z+TMP_V2.z+pz), u2, v, r, g, b, a);
					baseOffset += currentVertexSize;
					putVertex(vertices, baseOffset, TMP_V6.set(-TMP_V1.x+TMP_V2.x+px, -TMP_V1.y+TMP_V2.y+py, -TMP_V1.z+TMP_V2.z+pz), u, v, r, g, b, a);
				}
			}
		}
	}

	@Override
	protected void flush(int[] offsets){

		//fill vertices
		if(useGPU){
			//if(mode != AlignMode.ParticleDirection)
			fillVerticesGPU(offsets);
			//else
			//fillVerticesToParticleDirectionGPU(offsets);
		}
		else {
			if(mode == AlignMode.Screen)
				fillVerticesToScreenCPU(offsets);
			else if(mode == AlignMode.ViewPoint)
				fillVerticesToViewPointCPU(offsets);
			//else
			//fillVerticesToParticleDirectionCPU(offsets);
		}

		//send vertices to meshes
		int addedVertexCount = 0;
		int vCount = bufferedParticlesCount*4;
		for(int v = 0; v < vCount; v += addedVertexCount){
			addedVertexCount = Math.min(vCount-v, MAX_VERTICES_PER_MESH);
			Renderable renderable = renderablePool.obtain();
			renderable.meshPartSize = (addedVertexCount/4)*6;
			renderable.mesh.setVertices(vertices, currentVertexSize *v, currentVertexSize * addedVertexCount);
			renderables.add(renderable);
		}
	}

	@Override
	public void getRenderables (Array<Renderable> renderables, Pool<Renderable> pool) {
		for(Renderable renderable : this.renderables)
			renderables.add(pool.obtain().set(renderable));
	}

	@Override
	public void save (AssetManager manager, ResourceData resources) {
		SaveData data = resources.createSaveData("billboardBatch");
		data.save("cfg", new Config(useGPU, mode));
		data.saveAsset(manager.getAssetFileName(texture), Texture.class);
	}

	@Override
	public void load (AssetManager manager, ResourceData resources) {
		SaveData data = resources.getSaveData("billboardBatch");
		if(data != null){
			setTexture((Texture)manager.get(data.loadAsset()));
			Config cfg = (Config)data.load("cfg");
			setUseGpu(cfg.useGPU);
			setAlignMode(cfg.mode);
		}
	}
}
